import { Injectable } from '@angular/core';
import { DataService, Technique, Tactic, Matrix, Domain } from "./data.service";
declare var tinygradient: any; //use tinygradient
// import * as tinygradient from 'tinygradient'
declare var tinycolor: any; //use tinycolor2
// import * as tinycolor from 'tinycolor2';
// import * as FileSaver from 'file-saver';
declare var math: any; //use mathjs
import * as globals from './globals'; //global variables
import * as is from 'is_js';

@Injectable()
export class ViewModelsService {

    constructor(private dataService: DataService) {

        // attempt to restore viewmodels
        // console.log(this.getCookie("viewModels"))
        // this.saveViewModelsCookies()
    }

    viewModels: ViewModel[] = [];
    /**
     * Create and return a new viewModel
     * @param {string} name the viewmodel name
     * @return {ViewModel} the created ViewModel
     */
    newViewModel(name: string, domainID: string) {
        let vm = new ViewModel(name, "vm"+ this.getNonce(), domainID, this.dataService);
        this.viewModels.push(vm);
        return vm;
    }

    nonce: number = 0;
    /**
     * Get a nonce.
     * @return a number that will never be regenerated by sequential calls to getNonce.
     *         Note: this applies on a session-by-session basis, nonces are not
     *         unique between app instances.
     */
    getNonce(): number {
        return this.nonce++;
    }



    /**
     * Destroy the viewmodel completely Nessecary if tab is closed!
     * @param vm viewmodel to destroy.
     */
    destroyViewModel(vm: ViewModel): void {
        for (let i = 0; i < this.viewModels.length; i++) {
            if (this.viewModels[i] == vm) {
                // console.log("destroying viewmodel", vm)
                this.viewModels.splice(i,1)
                return;
            }
        }
    }

    /**
     * layer combination operation
     * @param  scoreExpression math expression of score expression
     * @param  scoreVariables  variables in math expression, mapping to viewmodel they correspond to
     * @param  comments           what viewmodel to inherit comments from
     * @param  coloring           what viewmodel to inherit manual colors from
     * @param  enabledness        what viewmodel to inherit state from
     * @param  layerName          new layer name
     * @param  filters            viewmodel to inherit filters from
     * @return                    new viewmodel inheriting above properties
     */
    layerLayerOperation(domainID: string, scoreExpression: string, scoreVariables: Map<string, ViewModel>, comments: ViewModel, gradient: ViewModel, coloring: ViewModel, enabledness: ViewModel, layerName: string, filters: ViewModel, legendItems: ViewModel): ViewModel {
        let result = new ViewModel("layer by operation", "vm" + this.getNonce(), domainID, this.dataService);

        if (scoreExpression) {
            scoreExpression = scoreExpression.toLowerCase() //should be enforced by input, but just in case
            let score_min = Infinity;
            let score_max = -Infinity;
            
            //get list of all technique IDs used in the VMs
            let techniqueIDs = new Set<string>(); 
            scoreVariables.forEach(function(vm, key) {
                vm.techniqueVMs.forEach(function(techniqueVM, techniqueID) {
                    techniqueIDs.add(techniqueID);
                })
            })
            //attempt to evaluate without a scope to catch the case of a static assignment
            try {
                // evaluate with an empty scope
                let mathResult = math.eval(scoreExpression, {});
                // if it didn't except after this, it evaluated to a single result.
                console.log("score expression evaluated to single result to be applied to all techniques");
                if (is.boolean(mathResult)) {
                    mathResult = mathResult ? "1" : "0"; //boolean to binary
                } else if (is.not.number(mathResult)) { //user inputted something weird, complain about it
                    throw {message: "math result ( " + mathResult + " ) is not a number"};
                }
                // if it didn't error, and outputted a single value, apply this to all techniques.
                result.initializeScoresTo = String(mathResult); //initialize scores to this value
                score_min = mathResult;
                score_max = mathResult;
            } catch(err) { //couldn't evaluate with empty scope, build scope for each technique
                // compute the score of each techniqueID
                techniqueIDs.forEach(function(technique_id) {
                    let new_tvm = new TechniqueVM(technique_id);
                    let scope = {};
                    let misses = 0; //number of times a VM is missing the value
                    scoreVariables.forEach(function(vm, key) {
                        let scoreValue: number;
                        if (!vm.hasTechniqueVM_id(technique_id)) { //missing technique
                            scoreValue = 0;
                            misses++;
                        } else { //technique exists
                            let score = vm.getTechniqueVM_id(technique_id).score;
                            if (score == "") {
                                scoreValue = 0;
                                misses++;
                            } else if (isNaN(Number(score))) {
                                scoreValue = 0;
                                misses++;
                            } else {
                                scoreValue = Number(score);
                            }
                        }
                        scope[key] = scoreValue;
                    });
                    //don't record a result if none of VMs had a score for this technique
                    //did at least one technique have a score for this technique?
                    if (misses < scoreVariables.size) { 
                        // console.log(scope);
                        let mathResult = math.eval(scoreExpression, scope);
                        if (is.boolean(mathResult)) {
                            mathResult = mathResult ? "1" : "0"; //boolean to binary
                        } else if (is.not.number(mathResult)) { //user inputted something weird, complain about it
                            throw {message: "math result ( " + mathResult + " ) is not a number"};
                        }
                        new_tvm.score = String(mathResult);
                        result.techniqueVMs.set(technique_id, new_tvm);

                        score_min = Math.min(score_min, mathResult);
                        score_max = Math.max(score_max, mathResult);
                    }
                })
            }
            //don't do gradient if there's no range of values
            if (score_min != score_max) {
                // set up gradient according to result range
                if (score_min != Infinity) result.gradient.minValue = score_min;
                if (score_max != -Infinity) result.gradient.maxValue = score_max;
                // if it's a binary range, set to whiteblue gradient
                if (score_min == 0 && score_max == 1) result.gradient.setGradientPreset("whiteblue");
            }
        }


        /**
         * Inherit a field from a vm
         * @param  {ViewModel} inherit_vm the viewModel to inherit from
         * @param  {string}    fieldname  the field to inherit from the viewmodel
         */
        function inherit(inherit_vm: ViewModel, fieldname: string) {
            // console.log("inherit", fieldname)
            inherit_vm.techniqueVMs.forEach(function(inherit_TVM) {
                let tvm = result.hasTechniqueVM_id(inherit_TVM.technique_tactic_union_id) ? result.getTechniqueVM_id(inherit_TVM.technique_tactic_union_id) : new TechniqueVM(inherit_TVM.technique_tactic_union_id)
                tvm[fieldname] = inherit_TVM[fieldname];
                // console.log(inherit_TVM.techniqueName, "->", tvm)
                result.techniqueVMs.set(inherit_TVM.technique_tactic_union_id, tvm);
            })
        }

        if (comments)    inherit(comments, "comment")
        if (coloring)    inherit(coloring, "color")
        if (enabledness) inherit(enabledness, "enabled")

        if (filters) { //copy filter settings
            result.filters.deSerialize(JSON.parse(filters.filters.serialize()))
        }

        if (legendItems) {
            result.legendItems = JSON.parse(JSON.stringify(legendItems.legendItems));
        }

        if (gradient) {
            result.gradient = new Gradient();
            result.gradient.deSerialize(gradient.gradient.serialize());
        }
        
        result.name = layerName;
        // console.log(result)
        this.viewModels.push(result)
        result.updateGradient();
        return result;
    } //end layer layer operation
}



/**
 * Gradient class used by viewmodels
 */
export class Gradient {
    //official colors used in gradients:

    colors: Gcolor[] = [new Gcolor("red"), new Gcolor("green")]; //current colors
    // options: string[] = ["white", "red", "orange", "yellow", "green", "blue", "purple"]; //possible colors
    options: string[] = ["#ffffff", "#ff6666", "#ffaf66","#ffe766", "#8ec843", "#66b1ff", "#ff66f4"]; //possible colors
    minValue: number = 0;
    maxValue: number = 100;
    gradient: any;
    gradientRGB: any;

    /**
     * Create a string version of this gradient
     * @return string version of gradient
     */
    serialize(): string {
        let colorList: string[] = [];
        let self = this;
        this.colors.forEach(function(gColor: Gcolor) {
            let hexstring = (tinycolor(gColor.color).toHexString())
            colorList.push(hexstring)
        });

        let rep = {
                "colors": colorList,
                "minValue": this.minValue,
                "maxValue": this.maxValue,
              }
        return JSON.stringify(rep, null, "\t")
    }

    /**
     * Restore this gradient from the given serialized representation
     * @param  rep serialized gradient
     */
    deSerialize(rep: string): void {
        let obj = JSON.parse(rep)
        let isColorStringArray = function(check): boolean {
            for (let i = 0; i < check.length; i++) {
                if (typeof(check[i]) !== "string" || !tinycolor(check[i]).isValid()) {
                    console.error("TypeError:", check[i], "(",typeof(check[i]),")", "is not a color-string")
                    return false;
                }
            }
            return true;
        }


        if (isColorStringArray(obj.colors)) {
            this.colors = []
            let self = this;
            obj.colors.forEach(function(hex: string) {
                self.colors.push(new Gcolor(hex));
            });
        } else console.error("TypeError: gradient colors field is not a color-string[]")
        this.minValue = obj.minValue;
        this.maxValue = obj.maxValue;
        this.updateGradient();
    }

    //presets in dropdown menu
    presets = {
        redgreen: [new Gcolor("#ff6666"), new Gcolor("#ffe766"), new Gcolor("#8ec843")],
        greenred: [new Gcolor("#8ec843"), new Gcolor("#ffe766"), new Gcolor("#ff6666")],
        bluered: [new Gcolor("#66b1ff"), new Gcolor("#ff66f4"), new Gcolor("#ff6666")],
        redblue: [new Gcolor("#ff6666"), new Gcolor("#ff66f4"), new Gcolor("#66b1ff")],
        whiteblue: [new Gcolor("#ffffff"), new Gcolor("#66b1ff")],
        whitered: [new Gcolor("#ffffff"), new Gcolor("#ff6666")]
    }

    /**
     * Convert a preset to tinycolor array
     * @param  preset preset name from preset array
     * @return        [description]
     */
    presetToTinyColor(preset) {
        let colorarray = []
        let self = this;
        this.presets[preset].forEach(function(gcolor: Gcolor) {
            colorarray.push(gcolor.color);
        });
        return tinygradient(colorarray).css('linear', 'to right');
    }

    constructor() { this.setGradientPreset('redgreen'); }

    /**
     * set this gradient to use the preset
     * @param  preset preset to use
     */
    setGradientPreset(preset: string): void {
        this.colors = this.presets[preset].map((color: Gcolor) => new Gcolor(color.color)); //deep copy gradient preset
        this.updateGradient();
    }

    /**
     * recompute gradient
     */
    updateGradient(): void {
        let colorarray = [];
        let self = this;
        this.colors.forEach(function(colorobj) {
            // figure out what kind of color this is
            // let format = tinycolor(colorobj.color).getFormat();
            // if (format == "name" && colorobj.color in self.labelToColor)
            colorarray.push(colorobj.color)
        });
        this.gradient = tinygradient(colorarray);
        this.gradientRGB = this.gradient.rgb(100);
    }

    /**
     * Add a color to the end of the gradient
     */
    addColor(): void {
        this.colors.push(new Gcolor(this.colors[this.colors.length - 1].color));
    }

    /**
     * Remove color at the given index
     * @param index index to remove color at
     */
    removeColor(index): void {
        this.colors.splice(index, 1)
    }

    // get the gradient color for a given value in the scale. Value is string format number
    getColor(valueString: string) {
        if (!this.gradient) this.updateGradient();

        let value: number;
        if (valueString.length == 0) return;
        else value = Number(valueString);

        if (value >= this.maxValue) { return this.gradientRGB[this.gradientRGB.length - 1]; }
        if (value <= this.minValue) { return this.gradientRGB[0]; }
        let index = (value - this.minValue)/(this.maxValue - this.minValue) * 100;
        // console.log(value, "->", index)
        return this.gradientRGB[Math.round(index)];
    }
}
//a color in the gradient
export class Gcolor {color: string; constructor(color: string) {this.color = color}};

//semi-synonymous with "layer"
export class ViewModel {
    // PROPERTIES & DEFAULTS

    name: string; // layer name
    domain: string = ""; // attack domain
    version: string = ""; // attack version
    domainID: string; // layer domain & version
    description: string = ""; //layer description
    uid: string; //unique identifier for this ViewModel. Do not serialize, let it get initialized by the VmService

    filters: Filter;

    metadata: Metadata[] = [];

    /*
     * sorting int meanings (see filterTechniques()):
     * 0: ascending alphabetically
     * 1: descending alphabetically
     * 2: ascending numerically
     * 3: descending numerically
     */
    sorting: number = 0;
    
    layout: LayoutOptions = new LayoutOptions();


    hideDisabled: boolean = false; //are disabled techniques hidden?


    gradient: Gradient = new Gradient(); //gradient for scores

    backgroundPresets: string[] = ['#e60d0d', '#fc3b3b', '#fc6b6b', '#fca2a2', '#e6550d', '#fd8d3c', '#fdae6b', '#fdd0a2', '#e6d60d', '#fce93b', '#fcf26b', '#fcf3a2', '#31a354', '#74c476', '#a1d99b', '#c7e9c0', '#3182bd', '#6baed6', '#9ecae1', '#c6dbef', '#756bb1', '#9e9ac8', '#bcbddc', '#dadaeb', '#636363', '#969696', '#bdbdbd', '#d9d9d9'];
    legendColorPresets: string[] = [];

    selectTechniquesAcrossTactics: boolean = true;
    selectSubtechniquesWithParent: boolean = false;

    needsToConstructTechniqueVMs = false;
    legacyTechniques = [];

    initializeScoresTo = ""; //value to initialize scores to

    techIDtoUIDMap: Object = {};
    techUIDtoIDMap: Object = {};

    constructor(name: string, uid: string, domainID: string, private dataService: DataService) {
        this.domainID = domainID;
        console.log("initializing ViewModel '" + name + "'");
        this.filters = new Filter();
        this.name = name;
        this.uid = uid;
    }

    loadVMData() {
        if (!this.domainID || !this.dataService.getDomain(this.domainID).dataLoaded) {
            console.log("subscribing to data loaded callback")
            let self = this;
            this.dataService.onDataLoad(this.domainID, function() {
                self.initTechniqueVMs()
                self.filters.initPlatformOptions(self.dataService.getDomain(self.domainID));
            }); 
        } else {
            this.initTechniqueVMs();
            this.filters.initPlatformOptions(this.dataService.getDomain(this.domainID));
        }
    }

    initTechniqueVMs() {
        console.log(this.name, "initializing technique VMs");
        for (let technique of this.dataService.getDomain(this.domainID).techniques) {
            for (let id of technique.get_all_technique_tactic_ids()) {
                let techniqueVM = new TechniqueVM(id);
                techniqueVM.score = this.initializeScoresTo;
                this.setTechniqueVM(techniqueVM, false);
            }
            //init subtechniques
            for (let subtechnique of technique.subtechniques) {
                for (let id of subtechnique.get_all_technique_tactic_ids()) {
                    let techniqueVM = new TechniqueVM(id);
                    techniqueVM.score = this.initializeScoresTo;
                    this.setTechniqueVM(techniqueVM, false);
                }
            }
        }
    }

    // changeTechniqueIDSelectionLock() {
    //     this.selectTechniquesAcrossTactics = !this.selectTechniquesAcrossTactics;
    // }

    showTacticRowBackground: boolean = false;
    tacticRowBackground: string = "#dddddd";

    // getTechniqueIDFromUID(technique_tactic_union_id: string){
    //     return this.techIDtoUIDMap[technique_tactic_union_id];
    // }

    // getTechniquesUIDFromID(technique_id: string){
    //     return this.techIDtoUIDMap[technique_id];
    // }

    // setTechniqueMaps(techIDtoUIDMapt, techUIDtoIDMapt){
    //     this.techIDtoUIDMap = Object.freeze(techIDtoUIDMapt);
    //     this.techUIDtoIDMap = Object.freeze(techUIDtoIDMapt);
    // }

     //  _____ ___ ___ _  _ _  _ ___ ___  _   _ ___     _   ___ ___
     // |_   _| __/ __| || | \| |_ _/ _ \| | | | __|   /_\ | _ \_ _|
     //   | | | _| (__| __ | .` || | (_) | |_| | _|   / _ \|  _/| |
     //   |_| |___\___|_||_|_|\_|___\__\_\\___/|___| /_/ \_\_| |___|

    techniqueVMs: Map<string, TechniqueVM> = new Map<string, TechniqueVM>(); //configuration for each technique
    // Getter
    public getTechniqueVM(technique: Technique, tactic: Tactic): TechniqueVM {
        if (!this.hasTechniqueVM(technique, tactic)) throw Error("technique VM not found: " + technique.attackID + ", " + tactic.attackID);
        return this.techniqueVMs.get(technique.get_technique_tactic_id(tactic));
    }
    public getTechniqueVM_id(technique_tactic_id: string): TechniqueVM {
        if (!this.hasTechniqueVM_id(technique_tactic_id)) throw Error("technique VM not found: " + technique_tactic_id);
        return this.techniqueVMs.get(technique_tactic_id);
    }
    /**
     * setter
     * @param {techniqueVM} techniqueVM: the techniqueVM to set
     * @param {boolean} overwrite (default true) if true, overwrite existing techniqueVMs under that ID.
     */
    public setTechniqueVM(techniqueVM: TechniqueVM, overwrite=true): void {
        if (this.techniqueVMs.has(techniqueVM.technique_tactic_union_id)) {
            if (overwrite) this.techniqueVMs.delete(techniqueVM.technique_tactic_union_id)
            else return;
        }
        this.techniqueVMs.set(techniqueVM.technique_tactic_union_id, techniqueVM);
    }
    //checker
    public hasTechniqueVM(technique: Technique, tactic: Tactic): boolean {
        return this.techniqueVMs.has(technique.get_technique_tactic_id(tactic));
    }
    public hasTechniqueVM_id(technique_tactic_id: string): boolean {
        return this.techniqueVMs.has(technique_tactic_id);
    }

    //  ___ ___ ___ _____ ___ _  _  ___     _   ___ ___
    // | __|   \_ _|_   _|_ _| \| |/ __|   /_\ | _ \_ _|
    // | _|| |) | |  | |  | || .` | (_ |  / _ \|  _/| |
    // |___|___/___| |_| |___|_|\_|\___| /_/ \_\_| |___|


    public highlightedTactic: Tactic = null;
    public highlightedTechnique: Technique = null;

    /**
     * Highlight the given technique under the given tactic
     * @param {Technique} technique to highlight
     * @param {Tactic} tactic wherein the technique occurs
     */
    public highlightTechnique(technique: Technique, tactic: Tactic) {
        this.highlightedTechnique = technique;
        this.highlightedTactic = tactic;
    }
    /**
     * Clear the technique highlight
     */
    public clearHighlight() {
        this.highlightedTactic = null;
        this.highlightedTechnique = null;
    }

    /**
     * currently selected techniques in technique_tactic_id format 
     */
    private selectedTechniques: Set<string> = new Set<string>(); 

    /**
     * Select the given technique. Depending on selectTechniquesAcrossTactics, either selects in all tactics or in given tactic
     * @param {Technique} technique to select
     * @param {Tactic} tactic wherein the technique occurs
     */
    public selectTechnique(technique: Technique, tactic: Tactic): void {
        if (this.selectTechniquesAcrossTactics) this.selectTechniqueAcrossTactics(technique);
        else (this.selectTechniqueInTactic(technique, tactic));
    }

    /**
     * unselect the given technique. Depending on selectTechniquesAcrossTactics, either unselects in all tactics or in given tactic
     * @param {Technique} technique to select
     * @param {Tactic} tactic wherein the technique occurs
     */
    public unselectTechnique(technique: Technique, tactic: Tactic): void {
        if (this.selectTechniquesAcrossTactics) this.unselectTechniqueAcrossTactics(technique);
        else (this.unselectTechniqueInTactic(technique, tactic));
    }

    /**
     * select the given technique in the given tactic
     * @param {Technique} technique to select
     * @param {Tactic} tactic wherein the technique occurs
     * @param {boolean} walkChildren (recursion helper) if true and selectSubtechniquesWithParent is true, walk selection up to parent technique
     */
    public selectTechniqueInTactic(technique: Technique, tactic: Tactic, walkChildren=true): void {
        if (this.selectSubtechniquesWithParent && walkChildren) { //check parent / children / siblings
            if (technique.isSubtechnique) { //select from parent
                this.selectTechniqueInTactic(technique.parent, tactic, true);
                return;
            } else { //select subtechniques
                for (let subtechnique of technique.subtechniques) {
                    this.selectTechniqueInTactic(subtechnique, tactic, false);
                }
            }
        }
        this.selectedTechniques.add(technique.get_technique_tactic_id(tactic));
    }

    /**
     * select all techniques under the given tactic
     * @param {Tactic} tactic wherein the techniques occur
     */
    public selectAllTechniquesInTactic(tactic: Tactic): void {
        for (let technique of tactic.techniques) {
            this.selectTechnique(technique, tactic);
        }
    }

    /**
     * select the given technique across all tactics in which it occurs
     * @param {Technique} technique to select
     * @param {boolean} walkChildren (recursion helper) if true and selectSubtechniquesWithParent is true, walk selection up to parent technique
     */
    public selectTechniqueAcrossTactics(technique: Technique, walkChildren=true): void {
        if (this.selectSubtechniquesWithParent && walkChildren) { //walk to parent / children / siblings
            if (technique.isSubtechnique) { //select from parent
                this.selectTechniqueAcrossTactics(technique.parent, true);
                return;
            } else { //select subtechniques
                for (let subtechnique of technique.subtechniques) {
                    this.selectTechniqueAcrossTactics(subtechnique, false);
                }
            }
        }
        for (let id of technique.get_all_technique_tactic_ids()) {
            this.selectedTechniques.add(id);
        }
    }
    
    /**
     * unselect the given technique in the given tactic
     * @param {Technique} technique to unselect
     * @param {Tactic} tactic wherein the technique occurs
     * @param {boolean} walkChildren (recursion helper) if true and selectSubtechniquesWithParent is true, walk selection up to parent technique
     */
    public unselectTechniqueInTactic(technique: Technique, tactic: Tactic, walkChildren=true): void {
        if (this.selectSubtechniquesWithParent && walkChildren) { //walk to parent / children / siblings
            if (technique.isSubtechnique) { //select from parent
                this.unselectTechniqueInTactic(technique.parent, tactic, true);
                return;
            } else { //select subtechniques
                for (let subtechnique of technique.subtechniques) {
                    this.unselectTechniqueInTactic(subtechnique, tactic, false);
                }
            }
        }
        this.selectedTechniques.delete(technique.get_technique_tactic_id(tactic));
    }

    /**
     * unselect all techniques in the given tactic
     * @param {Tactic} tactic wherein the techniques occur
     */
    public unselectAllTechniquesInTactic(tactic: Tactic): void {
        for (let technique of tactic.techniques) {
            this.unselectTechnique(technique, tactic);
        }
    }
    
    /**
     * unselect the given technique across all tactics in which it occurs
     * @param {Technique} technique to unselect
     * @param {boolean} walkChildren (recursion helper) if true and selectSubtechniquesWithParent is true, walk selection up to parent technique
     */
    public unselectTechniqueAcrossTactics(technique: Technique, walkChildren=true) {
        if (this.selectSubtechniquesWithParent && walkChildren) { //walk to parent / children / siblings
            if (technique.isSubtechnique) { //select from parent
                this.unselectTechniqueAcrossTactics(technique.parent, true);
                return;
            } else { //select subtechniques
                for (let subtechnique of technique.subtechniques) {
                    this.unselectTechniqueAcrossTactics(subtechnique, false);
                }
            }
        }
        for (let id of technique.get_all_technique_tactic_ids()) {
            this.selectedTechniques.delete(id);
        }
    }

    /**
     * unselect all techniques
     */
    public clearSelectedTechniques() {
        this.selectedTechniques.clear();
    }

    /**
     * Select all techniques
     */
    public selectAllTechniques(): void {
        this.clearSelectedTechniques()
        this.invertSelection();
    }

    /**
     * Set all selected techniques to deselected, and select all techniques not currently selected
     */
    public invertSelection(): void {
        let previouslySelected = new Set(this.selectedTechniques);
        this.clearSelectedTechniques();
        let self = this;
        this.techniqueVMs.forEach(function(tvm, key) {
            if (!previouslySelected.has(tvm.technique_tactic_union_id)) self.selectedTechniques.add(tvm.technique_tactic_union_id)
        });
    }

    /**
     * Select all techniques with annotations if nothing is currently selected, or select a subset of
     * the current selection that has annotations
     */
    public selectAnnotated(): void {
        let self = this;
        if (this.isCurrentlyEditing()) {
            // deselect techniques without annotations
            let selected = new Set(this.selectedTechniques);
            this.techniqueVMs.forEach(function(tvm, key) {
                if (selected.has(tvm.technique_tactic_union_id) && !tvm.annotated()) self.selectedTechniques.delete(tvm.technique_tactic_union_id);
            })
        } else {
            // select all techniques with annotations
            this.techniqueVMs.forEach(function(tvm, key) {
                if (tvm.annotated()) self.selectedTechniques.add(tvm.technique_tactic_union_id);
            });
        }
    }

    /**
     * Select all techniques without annotations if nothing is currently selected, or select a subset of
     * the current selection that do not have annotations
     */
    public selectUnannotated(): void {
        let self = this;
        if (this.isCurrentlyEditing()) {
            // deselect techniques with annotations
            let selected = new Set(this.selectedTechniques);
            this.techniqueVMs.forEach(function(tvm, key) {
                if (selected.has(tvm.technique_tactic_union_id) && tvm.annotated()) self.selectedTechniques.delete(tvm.technique_tactic_union_id);
            })
        } else {
            // select all techniques without annotations
            this.selectAnnotated();
            this.invertSelection();
        }
    }

    /**
     * Return true if the given technique is selected, false otherwise
     * @param  {Technique}  technique the technique to check
    * * @param  {Tactic}  tactic wherein the technique occurs
     * @return {boolean}           true if selected, false otherwise
     */
    public isTechniqueSelected(technique: Technique, tactic: Tactic, walkChildren=true): boolean {
        if (this.selectTechniquesAcrossTactics) {
            if (this.selectSubtechniquesWithParent && walkChildren) { //check parent / children / siblings
                if (technique.isSubtechnique) { //select from parent
                    return this.isTechniqueSelected(technique.parent, tactic, true);
                } else {
                    for (let subtechnique of technique.subtechniques) {
                        if (this.isTechniqueSelected(subtechnique, tactic, false)) return true;
                    }
                }
            }

            for (let id of technique.get_all_technique_tactic_ids()) {
                if (this.selectedTechniques.has(id)) return true;
            }
            return false;
        } else {
            if (this.selectSubtechniquesWithParent && walkChildren) { //check parent / children / siblings
                if (technique.isSubtechnique) { //select from parent
                    return this.isTechniqueSelected(technique.parent, tactic, true);
                } else {
                    for (let subtechnique of technique.subtechniques) {
                        if (this.isTechniqueSelected(subtechnique, tactic, false)) return true;
                    }
                }
            }
            return this.selectedTechniques.has(technique.get_technique_tactic_id(tactic));
        }
    }

    /**
     * return the number of selected techniques
     * @return {number} the number of selected techniques
     */
    public getSelectedTechniqueCount(): number {
        if (this.selectTechniquesAcrossTactics) {
            if (this.selectSubtechniquesWithParent) {
                // match across tactics
                // match subtechniques and parents
                
                // matches this part
                // vvvvv     
                // T1001.001^TA1000
                let ids = new Set();
                this.selectedTechniques.forEach((unionID) => ids.add(unionID.split("^")[0].split(".")[0]));
                return ids.size;
            } else {
                // match across tactics
                // differentiate subtechniques and parents

                // matches this part
                // vvvvv vvv
                // T1001.001^TA1000
                let ids = new Set();
                this.selectedTechniques.forEach((unionID) => ids.add(unionID.split("^")[0]));
                return ids.size;
            }
        } else {
            if (this.selectSubtechniquesWithParent) {
                // differentiate tactics
                // match subtechniques and parents

                // matches this part
                // vvvvv     vvvvvv
                // T1001.001^TA1000
                let ids = new Set();
                this.selectedTechniques.forEach((unionID) => {
                    let split = unionID.split("^");
                    let tacticID = split[1];
                    let techniqueID = split[0].split(".")[0];
                    ids.add(techniqueID + "^" + tacticID);
                })
                return ids.size;
            } else {
                // differentiate tactics
                // differentiate subtechniques and parents

                // matches this part
                // vvvvv vvv vvvvvv
                // T1001.001^TA1000
                return this.selectedTechniques.size;
            }
        }
    }

    /**
     * Returns true if the given tactic is selected
     * @param  {Tactic}  tactic to check
     * @return {boolean} true if selected
     */
    public isTacticSelected(tactic: Tactic) {
        let self = this;
        var result = tactic.techniques.every(function(technique) {
            return self.isTechniqueSelected(technique, tactic)
        });
        return result;
    }


    /**
     * Return true if currently editing any techniques, false otherwise
     * @return {boolean} true if currently editing any techniques, false otherwise
     */
    public isCurrentlyEditing(): boolean {
        return this.getSelectedTechniqueCount() > 0;
    }

    /**
     * edit the selected techniques
     * @param {string} field the field to edit
     * @param {any}    value the value to place in the field
     */
    public editSelectedTechniques(field: string, value: any): void {
        this.selectedTechniques.forEach((id) => {
            this.getTechniqueVM_id(id)[field] = value;
        })
    }

    /**
     * Reset the selected techniques' annotations to their default values
     */
    public resetSelectedTechniques(): void {
        this.selectedTechniques.forEach((id) => {
            this.getTechniqueVM_id(id).score = "";
            this.getTechniqueVM_id(id).comment = "";
            this.getTechniqueVM_id(id).color = "";
            this.getTechniqueVM_id(id).enabled = true;
        })
    }

    /**
     * Get get a common value from the selected techniques
     * @param  field the field to get the common value from
     * @return       the value of the field if all selected techniques have the same value, otherwise ""
     */
    public getEditingCommonValue(field: string): any {
        if (!this.isCurrentlyEditing()) return "";
        let ids = Array.from(this.selectedTechniques);
        let commonValue = this.getTechniqueVM_id(ids[0])[field];
        for (let i = 1; i < ids.length; i++) {
            if (this.getTechniqueVM_id(ids[i])[field] != commonValue) return ""
        }

        return commonValue;
    }

    /**
     * add a new blank metadata to the metadata list, for editing in UI
     */
    public addMetadata() {
        let m = new Metadata()
        this.metadata.push(m);
    }

    /**
     * remove a metadata from the metadata list
     * @param index the index to remove from the list
     */
    public removeMetadata(index: number) {
        this.metadata.splice(index, 1)
    }

    
    //  oooooooo8                          o8          o88 ooooooooooo o88   o888   o8                           
    // 888           ooooooo  oo oooooo  o888oo       o88   888    88  oooo   888 o888oo ooooooooo8 oo oooooo    
    //  888oooooo  888     888 888    888 888       o88     888ooo8     888   888  888  888oooooo8   888    888  
    //         888 888     888 888        888     o88       888         888   888  888  888          888         
    // o88oooo888    88ooo88  o888o        888o o88        o888o       o888o o888o  888o  88oooo888 o888o        
    //                                         o88                                                               
    //    ooooo ooooo            o888                                                 
    //     888   888  ooooooooo8  888 ooooooooo    ooooooooo8 oo oooooo    oooooooo8  
    //     888ooo888 888oooooo8   888  888    888 888oooooo8   888    888 888ooooooo  
    //     888   888 888          888  888    888 888          888                888 
    //    o888o o888o  88oooo888 o888o 888ooo88     88oooo888 o888o       88oooooo88  
    //                                o888                                            

    /**
     * filter tactics according to viewmodel state
     * @param {Tactic[]} tactics to filter
     * @param {Matrix} matrix that the tactics fall under
     * @returns {Tactic[]} filtered tactics
     */
    public filterTactics(tactics: Tactic[], matrix: Matrix): Tactic[] {
        return tactics.filter((tactic: Tactic) => this.filterTechniques(tactic.techniques, tactic, matrix).length > 0);
    }

    /**
     * filter techniques according to viewModel state
     * @param {Technique[]} techniques list of techniques to filter
     * @param {Tactic} tactic tactic the techniques fall under
     * @param {Matrix} matrix that the techniques fall under
     * @returns {Technique[]} filtered techniques
     */
    public filterTechniques(techniques: Technique[], tactic: Tactic, matrix: Matrix): Technique[] {
        return techniques.filter((technique: Technique) => {
            let techniqueVM = this.getTechniqueVM(technique, tactic);
            // filter by enabled
            if (this.hideDisabled && !techniqueVM.enabled) return false;
            if (matrix.name == "PRE-ATT&CK") return true; // don't filter by platform if it's pre-attack
            // filter by platform
            let platforms = new Set(technique.platforms)
            for (let platform of this.filters.platforms.selection) {
                if (platforms.has(platform)) return true; //platform match
            }
            return false; //no platform match
        })
    }

    /**
     * sort techniques accoding to viewModel state
     * @param {Technique[]} techniques techniques to sort
     * @param {Tactic} tactic tactic the techniques fall under
     * @returns {Technique[]} sorted techniques
     */
    public sortTechniques(techniques: Technique[], tactic: Tactic): Technique[] {
        return techniques.sort((technique1: Technique, technique2: Technique) => {
            let techniqueVM1 = this.getTechniqueVM(technique1, tactic);
            let techniqueVM2 = this.getTechniqueVM(technique2, tactic);
            let score1 = techniqueVM1.score.length > 0 ? Number(techniqueVM1.score) : 0;
            let score2 = techniqueVM2.score.length > 0 ? Number(techniqueVM2.score) : 0;
            switch(this.sorting) {
                default:
                case 0:
                    return technique1.name.localeCompare(technique2.name);
                case 1:
                    return technique2.name.localeCompare(technique1.name);
                case 2:
                    if (score1 === score2) return technique1.name.localeCompare(technique2.name);
                    else return score1 - score2;
                case 3:
                    if (score1 === score2) return technique1.name.localeCompare(technique2.name);
                    else return score2 - score1;
            }
        })
    }

    /**
     * apply sort and filter state to techniques
     * @param {Technique[]} techniques techniques to sort and filter
     * @param {Tactic} tactic that the techniques fall under
     * @param {Matrix} matrix that the techniques fall under
     * @returns {Technique[]} sorted and filtered techniques
     */
    public applyControls(techniques: Technique[], tactic: Tactic, matrix: Matrix): Technique[] {
        //apply sort and filter
        return this.sortTechniques(this.filterTechniques(techniques, tactic, matrix), tactic);
    }

    



    //  ___ ___ ___ ___   _   _    ___ ____  _ _____ ___ ___  _  _
    // / __| __| _ \_ _| /_\ | |  |_ _|_  / /_\_   _|_ _/ _ \| \| |
    // \__ \ _||   /| | / _ \| |__ | | / / / _ \| |  | | (_) | .` |
    // |___/___|_|_\___/_/ \_\____|___/___/_/ \_\_| |___\___/|_|\_|

    /**
     * stringify this vm
     * @return string representation
     */
    serialize(): string {
        let modifiedTechniqueVMs = []
        let self = this;
        this.techniqueVMs.forEach(function(value,key) {
            if (value.modified()) modifiedTechniqueVMs.push(JSON.parse(value.serialize())) //only save techniqueVMs which have been modified
        })
        let rep: {[k: string]: any } = {};
        rep.name = this.name;

        rep.versions = {
            "attack": this.dataService.getDomain(this.domainID).getVersion(),
            "navigator": globals.nav_version,
            "layer": globals.layer_version
        }

        rep.domain = this.domainID.substr(0, this.domainID.search(/-v[0-9]/g));
        rep.description = this.description;
        rep.filters = JSON.parse(this.filters.serialize());
        rep.sorting = this.sorting;
        rep.layout = this.layout.serialize();
        rep.hideDisabled = this.hideDisabled;
        rep.techniques = modifiedTechniqueVMs;
        rep.gradient = JSON.parse(this.gradient.serialize());
        rep.legendItems = JSON.parse(JSON.stringify(this.legendItems));
        rep.metadata = this.metadata.filter((m)=>m.valid()).map((m) => m.serialize());

        rep.showTacticRowBackground = this.showTacticRowBackground;
        rep.tacticRowBackground = this.tacticRowBackground;
        rep.selectTechniquesAcrossTactics = this.selectTechniquesAcrossTactics;
        rep.selectSubtechniquesWithParent = this.selectSubtechniquesWithParent;

        return JSON.stringify(rep, null, "\t");
    }

    /**
     * restore the domain and version from a string
     * @param rep string to restore from
     */
    deSerializeDomainID(rep: any): void {
        let obj = (typeof(rep) == "string")? JSON.parse(rep) : rep
        this.name = obj.name
        this.version = this.dataService.getCurrentVersion(); // layer with no specified version defaults to current version
        if ("versions" in obj) {
            if ("attack" in obj.versions) {
                if (typeof(obj.versions.attack) === "string") {
                    if (obj.versions.attack.length > 0) this.version = "v" + obj.versions.attack.match(/[0-9]/g)[0];
                }
                else console.error("TypeError: attack version field is not a string");
            }
            if(obj.versions["layer"] !== globals.layer_version){
                alert("WARNING: Uploaded layer version (" + String(obj.versions["layer"]) + ") does not match Navigator's layer version ("
                + String(globals.layer_version) + "). The layer configuration may not be fully restored.");
            }
        }
        if ("version" in obj) { // backwards compatibility with Layer Format 3
            if (obj.version !== globals.layer_version){
                alert("WARNING: Uploaded layer version (" + String(obj.version) + ") does not match Navigator's layer version ("
                + String(globals.layer_version) + "). The layer configuration may not be fully restored.");
            }
        }
        // patch for old domain name convention 
        if(obj.domain in this.dataService.domain_backwards_compatibility) {
            this.domain = this.dataService.domain_backwards_compatibility[obj.domain];
        } else { this.domain = obj.domain; }
        this.domainID = this.dataService.getDomainID(this.domain, this.version);
    }

    /**
     * restore this vm from a string
     * @param  rep string to restore from
     */
    deSerialize(rep: any): void {
        let obj = (typeof(rep) == "string")? JSON.parse(rep) : rep
        
        if ("description" in obj) {
            if (typeof(obj.description) === "string") this.description = obj.description;
            else console.error("TypeError: description field is not a string")
        }
        if ("filters" in obj) { this.filters.deSerialize(obj.filters); }
        if ("sorting" in obj) {
            if (typeof(obj.sorting) === "number") this.sorting = obj.sorting;
            else console.error("TypeError: sorting field is not a number")
        }
        if ("hideDisabled" in obj) {
            if (typeof(obj.hideDisabled) === "boolean") this.hideDisabled = obj.hideDisabled;
            else console.error("TypeError: hideDisabled field is not a boolean")
        }

        if ("gradient" in obj) {
            this.gradient = new Gradient();
            this.gradient.deSerialize(JSON.stringify(obj.gradient))
        }

        if ("legendItems" in obj) {
            for (let i = 0; i < obj.legendItems.length; i++) {
                let legendItem = {
                    color: "#defa217",
                    label: "default label"
                };
                if (!("label" in obj.legendItems[i])) {
                    console.error("Error: LegendItem required field 'label' not present")
                    continue;
                }
                if (!("color" in obj.legendItems[i])) {
                    console.error("Error: LegendItem required field 'label' not present")
                    continue;
                }

                if (typeof(obj.legendItems[i].label) === "string") {
                    legendItem.label = obj.legendItems[i].label;
                } else {
                    console.error("TypeError: legendItem label field is not a string")
                    continue
                }

                if (typeof(obj.legendItems[i].color) === "string" && tinycolor(obj.legendItems[i].color).isValid()) {
                    legendItem.color = obj.legendItems[i].color;
                } else {
                    console.error("TypeError: legendItem color field is not a color-string:", obj.legendItems[i].color, "(", typeof(obj.legendItems[i].color),")")
                    continue
                }
                this.legendItems.push(legendItem);
            }
        }

        if ("showTacticRowBackground" in obj) {
            if (typeof(obj.showTacticRowBackground) === "boolean") this.showTacticRowBackground = obj.showTacticRowBackground
            else console.error("TypeError: showTacticRowBackground field is not a boolean")
        }
        if ("tacticRowBackground" in obj) {
            if (typeof(obj.tacticRowBackground) === "string" && tinycolor(obj.tacticRowBackground).isValid()) this.tacticRowBackground = obj.tacticRowBackground;
            else console.error("TypeError: tacticRowBackground field is not a color-string:", obj.tacticRowBackground, "(", typeof(obj.tacticRowBackground),")")
        }
        if ("selectTechniquesAcrossTactics" in obj) {
            if (typeof(obj.selectTechniquesAcrossTactics) === "boolean") this.selectTechniquesAcrossTactics = obj.selectTechniquesAcrossTactics
            else console.error("TypeError: selectTechniquesAcrossTactics field is not a boolean")
        }
        if ("selectSubtechniquesWithParent" in obj) {
            if (typeof(obj.selectSubtechniquesWithParent) === "boolean") this.selectSubtechniquesWithParent = obj.selectSubtechniquesWithParent
            else console.error("TypeError: selectSubtechniquesWithParent field is not a boolean")
        }
        if ("techniques" in obj) {
            if(obj.techniques.length > 0) {
                for (let i = 0; i < obj.techniques.length; i++) {
                    var obj_technique = obj.techniques[i];
                    if ("tactic" in obj_technique) {
                        let tvm = new TechniqueVM("");
                        tvm.deSerialize(JSON.stringify(obj_technique),
                                        obj_technique.techniqueID,
                                        obj_technique.tactic);
                        this.setTechniqueVM(tvm);
                    } else {
                        // occurs in multiple tactics
                        // match to Technique by attackID
                        for (let technique of this.dataService.getDomain(this.domainID).techniques) {
                            if (technique.attackID == obj_technique.techniqueID) {
                                // match technique
                                for (let tactic of technique.tactics) {
                                    let tvm = new TechniqueVM("");
                                    tvm.deSerialize(JSON.stringify(obj_technique),
                                                    obj_technique.techniqueID,
                                                    tactic);
                                    this.setTechniqueVM(tvm);
                                }
                                break;
                            }
                            //check against subtechniques
                            for (let subtechnique of technique.subtechniques) {
                                if (subtechnique.attackID == obj_technique.techniqueID) {
                                    for (let tactic of subtechnique.tactics) {
                                        let tvm = new TechniqueVM("");
                                        tvm.deSerialize(JSON.stringify(obj_technique),
                                                        obj_technique.techniqueID,
                                                        tactic);
                                        this.setTechniqueVM(tvm);
                                    }
                                    break;
                                }
                            }
                        }
                        
                    }
                }
            }
        }
        if ("metadata" in obj) {
            for (let metadataObj of obj.metadata) {
                let m = new Metadata();
                m.deSerialize(metadataObj);
                if (m.valid()) this.metadata.push(m)
            }
        }
        if ("layout" in obj) {
            this.layout.deserialize(obj.layout);
        }
        else if ("viewMode" in obj) {
            /*
             * viewMode backwards compatibility:
             * 0: full table (side layout, show name)
             * 1: compact table (side layout, show ID)
             * 2: mini table (mini layout, show neither name nor ID)
             */
            if (typeof(obj.viewMode) === "number") {
                switch(obj.viewMode) {
                    default:
                    case 0:
                        break; //default matrix layout already initialized
                    case 1:
                        this.layout.layout = "side";
                        this.layout.showName = false;
                        this.layout.showID = true;
                        break;
                    case 2:
                        this.layout.layout = "mini";
                        this.layout.showName = false;
                        this.layout.showID = false;
                }
            }
            else console.error("TypeError: viewMode field is not a number")
        }
        
        this.updateGradient();
    }

    /**
     * Add a color to the end of the gradient
     */
    addGradientColor(): void {
        this.gradient.addColor();
        this.updateGradient();
    }

    /**
     * Remove color at the given index
     * @param index index to remove color at
     */
    removeGradientColor(index: number): void {
        this.gradient.removeColor(index)
        this.updateGradient();
    }

    /**
     * Update this vm's gradient
     */
    updateGradient(): void {
        console.log("updating gradient")
        this.gradient.updateGradient();
        let self = this;
        this.techniqueVMs.forEach(function(tvm, key) {
            tvm.scoreColor = self.gradient.getColor(tvm.score);
        });
        this.updateLegendColorPresets();
    }

    legendItems = [

    ];

    addLegendItem(): void {
        var newObj = {
            label: "NewItem",
            color: '#00ffff'
        }
        this.legendItems.push(newObj);
    }

    deleteLegendItem(index: number): void {
        this.legendItems.splice(index,1);
    }

    clearLegend(): void {
        this.legendItems = [];
    }

    updateLegendColorPresets(): void {
        this.legendColorPresets = [];
        for(var i = 0; i < this.backgroundPresets.length; i++){
            this.legendColorPresets.push(this.backgroundPresets[i]);
        }
        for(var i = 0; i < this.gradient.colors.length; i++){
            this.legendColorPresets.push(this.gradient.colors[i].color);
        }
    }

    /**
     * return an acronym version of the given string
     * @param  words the string of words to get the acrnoym of
     * @return       the acronym string
     */
    acronym(words: string): string {
        let skipWords = ["on","and", "the", "with", "a", "an", "of", "in", "for", "from"]

        let result = "";
        let wordSplit = words.split(" ");
        if (wordSplit.length > 1) {
            let wordIndex = 0;
            // console.log(wordSplit);
            while (result.length < 4 && wordIndex < wordSplit.length) {
                if (skipWords.includes(wordSplit[wordIndex].toLowerCase())) {
                    wordIndex++;
                    continue;
                }

                //find first legal char of word
                for (let charIndex = 0; charIndex < wordSplit[wordIndex].length; charIndex++) {
                    let code = wordSplit[wordIndex].charCodeAt(charIndex);
                    if (code < 48 || (code > 57 && code < 65) || (code > 90 && code < 97) || code > 122) { //illegal character
                        continue;
                    } else {
                        result += wordSplit[wordIndex].charAt(charIndex).toUpperCase()
                        break;
                    }
                }

                wordIndex++;
            }

            return result;
        } else {
            return wordSplit[0].charAt(0).toUpperCase();
        }
    }
}

// the viewmodel for a specific technique
export class TechniqueVM {
    techniqueID: string;
    technique_tactic_union_id: string;
    tactic: string;

    score: string = "";
    scoreColor: any; //color for score gradient

    color: string = ""; //manually assigned color-class name
    enabled: boolean = true;
    comment: string = ""
    metadata: Metadata[] = [];

    showSubtechniques: boolean = false;

    //print this object to the console
    print(): void {
        console.log(this.serialize())
        console.log(this)
    }

    /**
     * Has this TechniqueVM been modified from its initialized state?
     * @return true if it has been modified, false otherwise
     */
    modified(): boolean {
        return (this.score != "" || this.color != "" || !this.enabled || this.comment != "" || this.showSubtechniques);
    }

    /**
     * Check if this TechniqueVM has been annotated
     * @return true if it has annotations, false otherwise
     */
    annotated(): boolean {
        return (this.score != "" || this.color != "" || !this.enabled || this.comment != "");
    }

    /**
     * Convert to string representation
     * @return string representation
     */
    serialize(): string {
        let rep: {[k: string]: any } = {};
        rep.techniqueID = this.techniqueID;
        rep.tactic = this.tactic;
        if (this.score !== "" && !(isNaN(Number(this.score)))) rep.score = Number(this.score);
        rep.color = this.color;
        rep.comment = this.comment;
        rep.enabled = this.enabled;
        rep.metadata = this.metadata.filter((m)=>m.valid()).map((m) => m.serialize());
        rep.showSubtechniques = this.showSubtechniques;
        //rep.technique_tactic_union_id = this.technique_tactic_union_id;
        //console.log(rep);
        return JSON.stringify(rep, null, "\t")
    }

    /**
     * Restore this technique from serialized technique
     * @param rep serialized technique string
     */
    deSerialize(rep: string, techniqueID: string, tactic: string): void {
        let obj = JSON.parse(rep);
        if (techniqueID !== undefined) this.techniqueID = techniqueID;
        else console.error("ERROR: TechniqueID field not present in technique")
        // if ("technique_tactic_union_id" in obj) this.technique_tactic_union_id = obj.technique_tactic_union_id;
        // else console.error("ERROR: technique_tactic_union_id field not present in technique")
        if ("tactic" !== undefined) this.tactic = tactic;
        else console.error("ERROR: tactic field not present in technique")
        if ("comment" in obj) {
            if (typeof(obj.comment) === "string") this.comment = obj.comment;
            else console.error("TypeError: technique comment field is not a number:", obj.comment, "(",typeof(obj.comment),")")
        }
        if ("color" in obj && obj.color !== "") {
            if (typeof(obj.color) === "string" && tinycolor(obj.color).isValid()) this.color = obj.color;
            else console.error("TypeError: technique color field is not a color-string:", obj.color, "(", typeof(obj.color),")")
        }
        if ("score" in obj) {
            if (typeof(obj.score) === "number") this.score = String(obj.score);
            else console.error("TypeError: technique score field is not a number:", obj.score, "(", typeof(obj.score), ")")
        }
        if ("enabled" in obj) {
            if (typeof(obj.enabled) === "boolean") this.enabled = obj.enabled;
            else console.error("TypeError: technique enabled field is not a boolean:", obj.enabled, "(", typeof(obj.enabled), ")");
        }
        if ("showSubtechniques" in obj) {
            if (typeof(obj.showSubtechniques) === "boolean") this.showSubtechniques = obj.showSubtechniques;
            else console.error("TypeError: technique showSubtechnique field is not a boolean:", obj.showSubtechniques, "(", typeof(obj.showSubtechniques), ")");
        }
        if(this.tactic !== undefined && this.techniqueID !== undefined){
            this.technique_tactic_union_id = this.techniqueID + "^" + this.tactic;
        } else {
            console.log("ERROR: Tactic and TechniqueID field needed.")
        }

        if ("metadata" in obj) {
            for (let metadataObj of obj.metadata) {
                let m = new Metadata();
                m.deSerialize(metadataObj);
                if (m.valid()) this.metadata.push(m)
            }
        }

    }

    constructor(technique_tactic_union_id: string) {
        this.technique_tactic_union_id = technique_tactic_union_id;
        var idSplit = technique_tactic_union_id.split("^");
        this.techniqueID = idSplit[0];
        this.tactic = idSplit[1];
    }
}

// the data for a specific filter
export class Filter {
    private readonly domain: string;
    platforms: {
        options: string[],
        selection: string[]
    }
    constructor() {
        this.platforms = {
            selection: [],
            options: []
        }
    }

    /**
     * Initialize the platform options according to the data in the domain
     * @param {Domain} domain the domain to parse for platform options
     */
    public initPlatformOptions(domain: Domain): void {
        this.platforms = {
            selection: JSON.parse(JSON.stringify(domain.platforms)),
            options: JSON.parse(JSON.stringify(domain.platforms))
        }
    }

    /**
     * toggle the given value in the given filter
     * @param {*} filterName the name of the filter
     * @param {*} value the value to toggle
     */
    toggleInFilter(filterName: string, value: string): void {
        if (!this[filterName].options.includes(value)) { console.log("not a valid option to toggle", value, this[filterName]); return }
        if (this[filterName].selection.includes(value)) {
            let index = this[filterName].selection.indexOf(value)
            this[filterName].selection.splice(index, 1);
        } else {
            this[filterName].selection.push(value);
        }
    }

    /**
     * determine if the given value is active in the filter
     * @param {*} filterName the name of the filter
     * @param {*} value the value to determine
     * @returns {boolean} true if value is currently enabled in the filter
     */
    inFilter(filterName, value): boolean {
        return this[filterName].selection.includes(value)
    }

    /**
     * Return the string representation of this filter
     * @return stringified filter
     */
    serialize(): string {
        return JSON.stringify({"platforms": this.platforms.selection})
    }

    /**
     * Replace the properties of this object with those of the given serialized filter
     * @param rep filter object
     */
    deSerialize(rep: any): void {
        // console.log(rep)
        let isStringArray = function(check): boolean {
            for (let i = 0; i < check.length; i++) {
                if (typeof(check[i]) !== "string") {
                    console.error("TypeError:", check[i], "(",typeof(check[i]),")", "is not a string")
                    return false;
                }

            }
            return true;
        }
        // let obj = JSON.parse(rep);
        if (rep.platforms) {
            if (isStringArray(rep.platforms)) {
                let backwards_compatibility_mappings = { //backwards compatibility with older layers
                    "android": "Android",
                    "ios": "iOS",

                    "windows": "Windows",
                    "linux": "Linux",
                    "mac": "macOS"
                }
                this.platforms.selection = rep.platforms.map(function(platform) {
                    if (platform in backwards_compatibility_mappings) {
                        return backwards_compatibility_mappings[platform];
                    } else {
                        return platform;
                    }
                });
            }
            else console.error("TypeError: filter platforms field is not a string[]");
        }
    }
}

// { name, value } with serialization
export class Metadata {
    public name: string;
    public value: string;
    constructor() {};
    serialize(): object { return {name: this.name, value: this.value} }
    deSerialize(rep: any) {
        if (rep.name) {
            if (typeof(rep.name) === "string") this.name = rep.name;
            else console.error("TypeError: Metadata field 'name' is not a string")
        } else console.error("Error: Metadata required field 'name' not present");
        if (rep.value) {
            if (typeof(rep.value) === "string") this.value = rep.value;
            else console.error("TypeError: Metadata field 'value' is not a string")
        } else console.error("Error: Metadata required field 'value' not present");
    }
    valid(): boolean { return this.name && this.name.length > 0 && this.value && this.value.length > 0 }
}

export class LayoutOptions {
    // current layout selection
    public readonly layoutOptions: string[] = ["side", "flat", "mini"];
    private _layout = this.layoutOptions[0]; //current selection
    public set layout(newLayout) {
        if (!this.layoutOptions.includes(newLayout)) {
            console.warn("invalid matrix layout", newLayout);
            return;
        }
        let oldLayout = this._layout;
        this._layout = newLayout;
        if (this._layout == "mini") { //mini-table cannot show ID or name
            this.showID = false;
            this.showName = false;
        }
        if (oldLayout == "mini" && newLayout != "mini") {
            this.showName = true; //restore default show value for name
        }
    }
    public get layout(): string { return this._layout; }
    
    //show technique/tactic IDs in the view?
    public _showID: boolean = false; 
    public set showID(newval: boolean) {
        this._showID = newval;
        if (newval == true && this._layout == "mini") this._layout = "side";
    }
    public get showID(): boolean { return this._showID; }
    
    //show technique/tactic names in the view?
    public _showName: boolean = true; 
    public set showName(newval: boolean) {
        this._showName = newval;
        if (newval == true && this._layout == "mini") this._layout = "side";
    }
    public get showName(): boolean { return this._showName; }

    public serialize(): object {
        return {
            "layout": this.layout,
            "showID": this.showID,
            "showName": this.showName
        }
    }
    public deserialize(rep: any) {
        if (rep.showID) {
            if (typeof(rep.showID) === "boolean") this.showID = rep.showID;
            else console.error("TypeError: layout field 'showID' is not a boolean:", rep.showID, "(", typeof(rep.showID), ")");
        }
        if (rep.showName) {
            if (typeof(rep.showName) === "boolean") this.showName = rep.showName;
            else console.error("TypeError: layout field 'showName' is not a boolean:", rep.showName, "(", typeof(rep.showName), ")");
        }
        //make sure this one goes last so that it can override name and ID if layout == 'mini'
        if (rep.layout) {
            if (typeof(rep.layout) === "string") this.layout = rep.layout;
            else console.error("TypeError: layout field 'layout' is not a string:", rep.layout, "(", typeof(rep.layout), ")");
        }
    }
}

